package com.imooc.pan.storage.engine.minio;

import cn.hutool.core.date.DateUtil;
import com.imooc.pan.core.constants.RPanConstants;
import com.imooc.pan.core.exception.RPanFrameworkException;
import com.imooc.pan.core.utils.FileUtils;
import com.imooc.pan.core.utils.UUIDUtil;
import com.imooc.pan.storage.engine.core.AbstractStorageEngine;
import com.imooc.pan.storage.engine.core.context.*;
import com.imooc.pan.storage.engine.minio.config.MinioConfig;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Janson
 * @Description Minio 对象存储引擎实现方案
 * @Date 2023/7/25
 */
@Slf4j
@Component
public class MinioStorageEngine extends AbstractStorageEngine {
    @Autowired
    private MinioClient minioClient;
    
    @Autowired
    private MinioConfig minioConfig;
    
    /**
     * 执行保存物理文件的动作
     * 下沉到具体的子类去实现
     * TODO 单文件上传模式，前端需要选择开启单文件上传 或者 分片上传，目前选择分片上传
     * @param context
     */
    
    @Override
    protected void doStore(StoreFileContext context) throws IOException {
        // TODO 第二级 目录加上用户 id，文件路径：files/user_id/2023/02/11/111.jpg
        String realPath = getFilePath(1L,FileUtils.getFileSuffix(context.getFilename()));
        String mimeType = getMimeType(FileUtils.getFileSuffix(context.getFilename()));
        PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                .bucket(minioConfig.bucket_files)
                .object(realPath)  // minio中文件路径
                .contentType(mimeType) // 文件类型，pdf、mp3，mp4
                .stream(context.getInputStream(),context.getTotalSize(),-1)
                .build();
    
        try {
            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            log.error("minio上传文件失败,文件名称：{}，文件大小：{}，失败时间：{}",context.getFilename(),context.getTotalSize(),DateUtil.now());
            throw new RuntimeException("minio上传文件失败",e);
        }
        context.setRealPath(realPath);
    }
    
    /**
     * 删除分片文件
     * @param context
     * @throws IOException
     */
    @Override
    protected void doDelete(DeleteFileContext context) throws IOException {
        // 获取chunks的路径
        List<String> chunkPaths = context.getRealFilePathList();
        // 获取删除对象
        List<DeleteObject> deleteChunks = Stream.iterate(0, i -> ++i)
                .limit(chunkPaths.size())
                .map(i -> new DeleteObject(chunkPaths.get(i)))
                .collect(Collectors.toList());
        // 构造删除对象
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                .bucket(minioConfig.bucket_files)
                .objects(deleteChunks)
                .build();
        // 执行删除
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
        results.forEach(r ->{
            try {
                DeleteError deleteError = r.get();
            } catch (Exception e) {
                log.error("分片文件删除失败，文件分片路径：{}",context.getRealFilePathList().get(0));
            }
        });
    }
    
    @Override
    protected void doStoreChunk(StoreFileChunkContext context) throws IOException {
        String realPath = getFileChunkPath(context.getUserId(),context.getChunkNumber(),
                                           context.getIdentifier());
        PutObjectArgs putObjectArgs = PutObjectArgs
                .builder()
                .bucket(minioConfig.bucket_files)
                .object(realPath) //分片名称为分片编号
                .stream(context.getInputStream(),context.getCurrentChunkSize(),-1)
                .build();
        try {
            minioClient.putObject(putObjectArgs);
            
        } catch (Exception e) {
            log.error("上传分片失败，文件名称：{}，分片编号：{}，分片大小：{}，时间：{}",context.getFilename(),context.getChunkNumber(),
                      context.getCurrentChunkSize(),DateUtil.now());
            throw new RuntimeException("上传分片失败",e);
        }
        
        context.setRealPath(realPath);
    }
    
    @Override
    protected void doMergeFile(MergeFileContext context) throws IOException {
        // 分块数量
        // Integer totalChunks = context.getTotalChunks();
        // 文件分片的存储路径
        List<String> chunkPaths = context.getRealPathList();
        
        // 文件 MD5
        // String identifier = context.getIdentifier();
        // Long userId = context.getUserId();
        String realFilePath = getFilePath(context.getUserId(),FileUtils.getFileSuffix(context.getFilename()));
    
        // 获取全部分片
        List<ComposeSource> chunkFiles = Stream.iterate(0, i -> ++i)
                .limit(chunkPaths.size())
                .map(i -> ComposeSource.builder().bucket(minioConfig.bucket_files).object(chunkPaths.get(i)).build())
                .collect(Collectors.toList());
        
        // 整合分片
        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs
                .builder()
                .bucket(minioConfig.bucket_files)
                .object(realFilePath)
                .sources(chunkFiles)
                .build();
        try {
            // 【注】 minio 分片文件，除最后一块外，其他分片必须大于 5 M，否则报错： size 1048576 must be greater than 5242880
            // 可以在前端进行限制。
            minioClient.composeObject(composeObjectArgs);
            context.setRealPath(realFilePath);
        } catch (Exception e) {
            log.error("合并文件分片失败，文件名称：{}，文件唯一标识：{}，时间：{}",context.getFilename(),context.getIdentifier(),DateUtil.now());
            throw new RuntimeException("合并文件分片失败",e);
        } finally {
            // deleteChunks(context.getTotalChunks(),context.getIdentifier(),context.getUserId(),context.getFilename());
        }
    
    }
    
   
    @Override
    protected void doReadFile(ReadFileContext context) throws IOException {
        // 文件真实存储路径
        String fileRealPath = context.getRealPath();
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(minioConfig.bucket_files)
                .object(fileRealPath)
                .build();
        try {
            InputStream stream = minioClient.getObject(getObjectArgs);
            IOUtils.copy(stream,context.getOutputStream());
        } catch (Exception e) {
            log.error("文件读取失败，文件名称：{}，失败原因：{}",context.getRealPath(),e);
            throw new RuntimeException("文件读取失败",e);
        }
        
    }
    
    /*****************************************private*****************************************/
    
    /**
     * 删除当前文件的全部分片
     * @param totalChunks
     * @param identifier
     * @param userId
     */
    private void deleteChunks(Integer totalChunks, String identifier,Long userId,String fileName){
    
        // 获取删除对象
        List<DeleteObject> deleteChunks = Stream.iterate(0, i -> ++i)
                .limit(totalChunks)
                .map(i -> new DeleteObject(getFileChunkPath(userId,i, identifier)))
                .collect(Collectors.toList());
        // 构造删除对象
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                .bucket(minioConfig.bucket_files)
                .objects(deleteChunks)
                .build();
        // 执行删除
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
        results.forEach(r ->{
            try {
                DeleteError deleteError = r.get();
            } catch (Exception e) {
                log.error("分片文件删除失败，文件名称：{}，文件标识：{}",fileName, identifier);
            }
        });
        
    }
    
    /**
     * 获取对象的完整名称
     * 年/月/日/UUID.fileSuffix
     *
     * @param fileSuffix
     * @return
     */
    private String getFilePath(Long userId,String fileSuffix) {
        return new StringBuffer()
                .append(DateUtil.thisYear())
                .append(RPanConstants.SLASH_STR)
                .append(DateUtil.thisMonth() + 1)
                .append(RPanConstants.SLASH_STR)
                .append(DateUtil.thisDayOfMonth())
                .append(RPanConstants.SLASH_STR)
                .append(userId)
                .append(RPanConstants.SLASH_STR)
                .append(UUIDUtil.getUUID())
                .append(fileSuffix)
                .toString();
    }
    private String getFileChunkPath(Long userId,Integer chunkNum,String identify) {
        return new StringBuffer()
                .append(identify.charAt(0))
                .append(RPanConstants.SLASH_STR)
                .append(identify.charAt(1))
                .append(RPanConstants.SLASH_STR)
                .append(identify)
                .append(RPanConstants.SLASH_STR)
                .append(userId)
                .append(RPanConstants.SLASH_STR)
                .append(chunkNum)
                .toString();
    }
    //根据扩展名获取mimeType
    private String getMimeType(String extension){
        if(extension == null){
            extension = "";
        }
        //根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType，字节流
        if(extensionMatch!=null){
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
        
    }
}
